package cn.tedu.charging.order.service;

import cn.tedu.charging.common.pojo.entity.ChargingProgressEsPO;
import cn.tedu.charging.common.pojo.message.ChargingCheckResultMessage;
import cn.tedu.charging.common.pojo.message.ChargingProgressMsg;
import cn.tedu.charging.common.pojo.message.ProgressData;
import cn.tedu.charging.common.pojo.param.ProgressCostParam;
import cn.tedu.charging.common.pojo.po.ChargingBillFailPO;
import cn.tedu.charging.common.pojo.po.ChargingBillSuccessPO;
import cn.tedu.charging.common.pojo.vo.ProgressCostVO;
import cn.tedu.charging.common.protocol.JsonResult;
import cn.tedu.charging.common.protocol.WebSocketMessage;
import cn.tedu.charging.order.dao.repository.ChargingProgressEsRepository;
import cn.tedu.charging.order.endpoint.WebSocketServer;
import cn.tedu.charging.order.feign.CostClient;
import cn.tedu.charging.order.feign.DeviceClient;
import cn.tedu.charging.order.mapper.BillFailMapper;
import cn.tedu.charging.order.mapper.BillSuccessMapper;
import cn.tedu.charging.order.mqtt.MqttProducerClient;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class MqttConsumerServiceImpl implements MqttConsumerService{
    @Autowired
    private BillSuccessMapper billSuccessMapper;
    @Autowired
    private BillFailMapper billFailMapper;
    @Autowired
    private DeviceClient deviceClient;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private WebSocketServer webSocketServer;
    @Autowired
    private CostClient costClient;
    @Autowired
    private ChargingProgressEsRepository chargingProgressEsRepository;

    @Override
    public void handleCheckResult(ChargingCheckResultMessage msg) {
        //1.添加分布式锁 setnx key value key:{orderNo} EX 5 锁名称 charging:order:lock:orderNo
        String lockKey = "charging:order:lock:"+msg.getOrderNo();
        Boolean haveLock = redisTemplate.opsForValue().setIfAbsent(lockKey,"", 5, TimeUnit.SECONDS);
        //2.判断锁是否获取成功
        if (!haveLock) {
            log.debug("抢锁失败，业务结束，由其他业务处理当前订单");
            return;
        }
        //3.获取锁成功,调用业务
        try {
            this.doHandle(msg);
        } catch (Exception e) {
            log.error("执行设备反馈消费异常",e);
        }finally {
            //4.释放锁 del key
            redisTemplate.delete(lockKey);
        }
    }

    /*处理充电进度消息*/
    @Override
    public void handleProgress(ChargingProgressMsg progressMsg) {
        log.debug("处理充电进度消息:{}",progressMsg);
        //1 计算价格
        log.debug("计算价格入参:{}",progressMsg);
        ProgressCostVO orderCost = getOrderCost(progressMsg);
        log.debug("计算价格入参:{},出参:{}",progressMsg,orderCost);

        //2 存储进度
        log.debug("保存进度入参 充电进度:{},价格数据:{}",progressMsg,orderCost);
        saveProgress(progressMsg,orderCost);
        log.debug("保存进度入参 充电进度:{},价格数据:{}成功",progressMsg,orderCost);

        //获取设备同步的充电进度 是否充满
        Boolean isFull = progressMsg.getIsFull();
        //3 判断是否充满
        if (isFull) {
            //充满了: 设备已经停止充电 订单没有必要通知设备 停止充电
            //3.1 修改 订单状态为 充电完成 自己实现  没充满的订单状态是什么？ 充电中 充满了 应该修改为 充电完成
            //3.2 修改 设备的状态 为 空闲  自己实现
            //3.3 扣除用户费用  分布式事务的场景 订单的状态(订单服务) 和 设备的状态(设备服务) 和 扣除余额的状态(用户服务) 都要成功
            //3.4 推送订单完成消息给用户 如果不走 继续收费, 免费停车半小时,超过收取停车费
            //3.5 扩展业务 给用户发送一张优惠券
            WebSocketMessage<String> webSocketMessage = new WebSocketMessage();
            webSocketMessage.setState(1); //充电完成 推荐使用常量或者枚举
            webSocketMessage.setMessage("A已经充满，订单完成，谢谢您的惠顾");
            //推送数据给用户
            pushProgress2User(progressMsg.getUserId(),webSocketMessage);

        }else {
            //4 没充满: 继续充电
            //4.1 设备安全检查 检查设备的温度
            checkDeviceTemperature(progressMsg.getPileId(),
                    progressMsg.getGunId(),progressMsg.getTemperature());

            //4.2 查询余额
            log.debug("用户余额入参:{}",progressMsg.getUserId());
            Double userBalance = getUserBalance(progressMsg.getUserId());
            log.debug("用户余额入参:{},出参:{}",progressMsg.getUserId(),userBalance);

            //4.3 判断用户余额是否充足 先付后享
            //1>> 先付后享 先充值 后充电 避免用户 薅羊毛
            //2>> 先享后付 共享单车,充电宝 外卖柜 芝麻分 信用分 先充电 后给钱  我们是无需每次设备同步充电进度都去进行余额校验
            //如果分够 卡里没钱 充电介绍后,订单没有支付
            // 2.1>用户欠平台的钱 平台可以扣分 芝麻分/信用分 ,
            // 2.2>下次再充电的时候,必须先把上一次的欠款的订单支付
            // 2.3>如果用户欠平台钱,用户以后再也不使用平台 换个平台 薅羊毛

            //先付后享 用户的余额 要大于 订单的话费
            if (orderCost != null) {
                if (userBalance < orderCost.getTotalCost().doubleValue()){
                    log.debug("通知用户,用户余额不足,停止充电");
                }
                if (userBalance >= orderCost.getTotalCost().doubleValue()){
                    log.debug("用户余额充足,可以继续充电");
                }
            }
            //4.4 推送进度数据给用户
            WebSocketMessage<ProgressData> progressMessage = new WebSocketMessage();
            progressMessage.setState(3); //充电中  推荐使用常量或者枚举

            //创建给用户同步充电进度的数据对象 ProgressData
            ProgressData data = new ProgressData();
            //设置充电度数 设备同步的实际充电度数
            data.setChargingCapacity(progressMsg.getChargingCapacity());
            // 思考 总度数从哪来 充电枪 读取的 车的数据 电池容量 电池剩余的电量 ...
            data.setTotalCapacity(100.00);
            //每度电的价格
            if (orderCost != null) {
                data.setOneElectricityCost(orderCost.getPowerFee().doubleValue());
                //总花费
                data.setTotalCost(orderCost.getTotalCost().doubleValue());
            }
            //设置时间
            data.setHours(0);
            data.setMinutes(15);
            data.setSeconds(12);
            //把充电进度数据设置到消息体
            progressMessage.setData(data);
            //推送给用户
            pushProgress2User(progressMsg.getUserId(),progressMessage);
        }
    }

    private void checkDeviceTemperature(Integer gunId, Integer temperature, Double pileId) {
        log.debug("检查设备温度：{}",temperature);
        //如果温度大于 40 小于 50 告警 可以继续冲
        //如果大于 50 停止充电 危险
        //小于40 正常
        if (temperature > 40 && temperature < 50){
            log.debug("告警,可以继续充电");
            log.debug("推送消息给用户");
        }
        if (temperature > 50){
            log.debug("温度过高");
            log.debug("推送消息给用户,停止充电");
        }
        if (temperature < 40){
            log.debug("温度正常");
        }
    }

    private ProgressCostVO getOrderCost(ChargingProgressMsg progressMsg) {
        //1.计订单价格 需要调用计价服务
        //2.通过Open-feign调用计价服务的计算价格接口
        //计算价格的入参
        ProgressCostParam progressCostParam = new ProgressCostParam();
        //把 设备同步的 数据 赋给 计算价格入参
        //参数少的时候
        //progressCostParam.setOrderNo(progressMsg.getOrderNo());
        //参数多的时候 BeanUtils.copyProperties 底层实现是 反射
        BeanUtils.copyProperties(progressMsg,progressCostParam);
        try {
            //通过Open-feign调用计价服务 Open-feign服务通信的组件
            JsonResult<ProgressCostVO> jsonResult =
                    costClient.calculateCost(progressCostParam);
            if (jsonResult != null) {
                //如果不判断是否为空 如果jsonResult为 null.getData() 会报空指针异常 NPE
                return jsonResult.getData();
            }
        } catch (Exception e) {
            log.error("调用费用服务计算价格失败, 参数: {}", progressCostParam, e);
        }
        return null;
    }

    private Double getUserBalance(Integer userId) {
        log.debug("获取用户余额:{}",userId);
        //todo 自己实现
        //1 获取 余额 要调用 用户服务 也可拆分 账户服务
        //2 通过Open-feign调用  用户服务/账户服务 的用户余额
        //3 获取余额 入参 用户id 出参 账户余额  调用方式 GET 调用地址 /user/balance/{userId}
        Double v = Double.valueOf("19000.01");
        //String.valueOf();
        //Integer.valueOf();
        return v;
    }

    /**
     * 保存 充电进度 和 价格信息
     * 应该把数据保存到数据库
     * 业务分析
     *   假定 1天 有 1000个订单 每个订单充满 30分钟 设备每1分钟同步一次充电进度
     *   问 一天会产生多少数据 ？
     *   1000 * 30 = 30000条的充电进度数据 + 1000个订单数据 = 31000条数据
     *  一个月 31000 * 30 = 930000
     *   930000 * 12 =  11,160,000 千万级
     *
     * 要存储这些 海量 的数据 应该选择哪种类型的数据库? 技术选型
     * 1、MySql <事务好,不适合量大> 关系型数据库 分库 分表  单库 单表的性能是有限制 mysql 底层基于 InnoDB B+树
     * 为了性能 不能太高 也不能太胖
     *      []
     *     [][]
     *    [][][]

     *
     * 2、ES NoSql 非关系型数据库 天然分布式集群  <量大>
     *
     * 3、Redis NoSql 基于 内存 非关系型数据库 <量大 不需要快>  Redis纯内存 贵
     * @param progressMsg
     */
    private void saveProgress(ChargingProgressMsg progressMsg,ProgressCostVO orderCost) {
        //创建 要保存到ES的数据对象 chargingProgressEsPO
        ChargingProgressEsPO chargingProgressEsPO = new ChargingProgressEsPO();
        //把 订单价格 的数据 赋给 chargingProgressEsPO
        BeanUtils.copyProperties(orderCost,chargingProgressEsPO);
        //把 设备同步的 数据 赋给 chargingProgressEsPO
        BeanUtils.copyProperties(progressMsg,chargingProgressEsPO);

        Long now = System.currentTimeMillis();
        //String strNow = now + "";
        String strNow = String.valueOf(now);
        //ID设置为当前的时间戳

        //时序数据 有时间 有顺序的数据
        //物联网(IoT)监控：如您代码中的充电进度数据
        //系统监控：CPU、内存使用率等指标
        //应用指标收集：API调用次数、响应时间
        //实时分析：业务指标趋势分析
        //influxDB 专门用来存储时序的数据库
        //InfluxDB 是一个开源的时序数据库（Time Series Database），
        // 专门用于处理时间序列数据

        //HBase 简介 列式数据库
        //HBase 是一个分布式的、面向列 的开源数据库，
        // 是 Google Bigtable 的开源实现。


        //时间              数据
        //设备同步数据的时间  充电进度数据
        //充电数据
        //时间顺序 数据
        //15:21   5
        //15:22   7

        //天气预报
        //时间顺序 数据
        //15:21   32度
        //15:22   34度

        //接口访问量
        //时间顺序 数据

        //监控订单
        //订单服务 export
        // 15:31:02 创建订单被调用
        // 15:31:03 创建订单被调用
        // 15:31:04 订单支付被调用
        // 15:32:01 创建订单被调用
        // 15:32:02 创建订单被调用
        // 15:32:03 创建订单被调用

        //统计1分钟 创建订单被调用的次数
        //15:31 调用了两次
        //15:32 调用了三次
        chargingProgressEsPO.setId(strNow);
        //保存
        chargingProgressEsRepository.save(chargingProgressEsPO);
    }

    private void pushProgress2User(Integer userId, WebSocketMessage webSocketMessage) {
        //推送充电完成消息给用户
        log.debug("推送充电完成消息给入参用户id:{} 消息内容:{}",userId,webSocketMessage);
        //通过 JSON库 Jackson Gson FastJson 把对象转化为Json字符串 把json字符串转化为对象
        byte[] serializser = MqttProducerClient.serializser(webSocketMessage);
        String json = new String(serializser);
        //webSocketServer.sendMsg2User(userId, JSON.toJSONString(webSocketMessage));
        webSocketServer.sendMsg2User(userId,json);
        log.debug("推送充电完成消息给入参用户id:{} 消息内容:{} 成功",userId,webSocketMessage);
    }

    public void doHandle(ChargingCheckResultMessage msg) {
        //1.检查订单是否存在
        boolean exists=checkBillExists(msg.getOrderNo());
        //2.不存在则更具result创建不同订单 做不同操作
        if(exists){
            log.debug("订单已经存在不能重复创建");
            return;
        }
        //3.判断结果 result true false
        if (msg.getResult()){
            //生成成功订单
            saveSuccessPO(msg);
            //TODO发送第二次命令要求设备可以开始充电了
        }else{
            //生成失败订单
            saveFailPO(msg);
            //推送消息给用户 通知换枪
            WebSocketMessage webSocketMessage=new WebSocketMessage();
            webSocketMessage.setState(1);//状态不同 前端处理逻辑不一样
            webSocketMessage.setMessage("不ok");
            webSocketMessage.setData("A设备启动失败,订单无法创建,请换枪,对不起,送您一张优惠券");
            webSocketServer.sendMsg2User(msg.getUserId(), JSON.toJSONString(webSocketMessage));
            //修改枪状态
            //deviceClient.updateGunStatus(1,3);
        }
    }
    private void saveFailPO(ChargingCheckResultMessage msg) {
        ChargingBillFailPO po = new ChargingBillFailPO();
        po.setBillId(msg.getOrderNo());
        po.setUserId(msg.getUserId());
        po.setCreateTime(new Date());
        po.setUpdateTime(new Date());
        po.setFailDesc("设备自检失败");
        billFailMapper.insert(po);
    }

    private void saveSuccessPO(ChargingCheckResultMessage msg) {
        ChargingBillSuccessPO po = new ChargingBillSuccessPO();
        po.setBillId(msg.getOrderNo());
        po.setUserId(msg.getUserId());
        po.setCreateTime(new Date());
        po.setUpdateTime(new Date());
        //订单检查生成成功 启动成功 这状态 可以充电
        po.setBillStatus(1);
        billSuccessMapper.insert(po);
    }

    private boolean checkBillExists(String orderNo) {
        QueryWrapper queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("bill_id",orderNo);
        //1.select count(1) from success where bill_id=? successCount
        Long sucCount = billSuccessMapper.selectCount(queryWrapper);
        //2.select count(1) from fail where bill_id=? failCount
        Long failCount = billFailMapper.selectCount(queryWrapper);
        return failCount>0||sucCount>0;
    }
}
